package com.api.apigateway.service.impl;
import java.util.Date;

import com.api.apigateway.enums.GateWayRefreshEnum;
import com.api.apigateway.mq.GatewayRefreshProducer;
import com.api.apigateway.service.RouteService;
import com.api.common.model.entity.InterfaceInfo;
import com.api.common.service.InnerInterfaceInfoService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RedisRouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;


/**
 * @author: 崔老爷
 * Date: 2024/10/12 15:17
 * @Description: 创建一个管理接口：通过此接口，允许用户上传新的 API 信息。上传后，将用户提供的接口信息存储到数据库中。
 * 动态路由配置：网关启动时或收到用户上传接口后，从数据库中读取最新的接口配置信息，并动态更新路由。
 */
@Service
@Slf4j
public class RouteServiceImpl implements RouteService {

    //负责操作网关的路由定义（增删改路由），并通过发布 RefreshRoutesEvent 事件来让网关重新加载路由。
    @Resource
    private RouteDefinitionWriter routeDefinitionWriter;
    //使用 Redis 来存储路由定义，并通过 RedisRouteDefinitionRepository用于将路由定义存储在 Redis 中
    @Resource
    private RedisRouteDefinitionRepository customRedisRouteDefinitionRepository;
    //这是用于查找所有路由定义的组件。它可以从 Redis 中检索已存储的路由。
//    @Resource
//    private RouteDefinitionLocator routeDefinitionLocator;

    // spring事件发布器 (单机项目使用这个)
    @Resource
    private ApplicationEventPublisher publisher;

    //(分布式用这个)
    @Resource
    private GatewayRefreshProducer gatewayRefreshProducer;

    @DubboReference(timeout = 5000, retries = 0)
//    @Lazy
    private InnerInterfaceInfoService innerInterfaceInfoService;

    private final static List<FilterDefinition> filters;

    static {
        List<FilterDefinition> tempFilters = new ArrayList<>();
        // 设置 Retry 过滤器的参数
        FilterDefinition retryFilterDefinition = new FilterDefinition("Retry");
        retryFilterDefinition.addArg("retries", "3");
        retryFilterDefinition.addArg("statuses", "BAD_GATEWAY,GATEWAY_TIMEOUT,INTERNAL_SERVER_ERROR");
        retryFilterDefinition.addArg("methods", "GET,POST");
        retryFilterDefinition.addArg("backoff.firstBackoff", "1000ms");
        retryFilterDefinition.addArg("backoff.maxBackoff", "3000ms");
        retryFilterDefinition.addArg("backoff.factor", "2");
        retryFilterDefinition.addArg("backoff.factor", "2");
        retryFilterDefinition.addArg("backoff.basedOnPreviousValue", "true");
        tempFilters.add(retryFilterDefinition);
        FilterDefinition circuitBreakerFilterDefinition = new FilterDefinition("CircuitBreaker");
        circuitBreakerFilterDefinition.addArg("name", "circuitBreakerRetry");
        circuitBreakerFilterDefinition.addArg("fallbackUri", "forward:/error/fallback");
        tempFilters.add(circuitBreakerFilterDefinition);
        /**
         * 虽然 filters 是 static 变量，但它是一个 List，因此列表中的元素仍然可以被修改。
         * 如果你不希望这些过滤器在运行时被修改，最好将 List 设置为不可变的。
         * 可以使用 Collections.unmodifiableList() 来确保不可变性
         */
        filters = Collections.unmodifiableList(tempFilters);
    }


    /**
     *  @PostConstruct 发生在 Bean 初始化完成但应用还没完全就绪的时候（Context 还在 refresh），此时：
     * Dubbo 的远程引用可能尚未可用 / Provider 还在启动或尚未注册到注册中心。
     * Nacos/注册中心初始化、服务订阅、路由表还在构建。
     * 你在 @PostConstruct 里调用远端 listInterfaceInfos()，很容易首调超时，进而让容器启动失败。
     *
     * @EventListener(ApplicationReadyEvent.class) 在 整个应用启动完成、WebServer 就绪、Dubbo/注册中心订阅完成后触发，这时再调 RPC 基本都能成功，所以“不会出问题”。
     */
    @EventListener(ApplicationReadyEvent.class)
    private void initRoutes(){
        log.info("---------------初始化路由-----------------");
        this.loadAndRefresh();
    }


    @Override
    public boolean loadAndRefresh() {
        //todo 这边要进行缓存 当前网关的地址，因为后期网关可能会部署多个且ip会动态变化，这边在刷新路由时把当前服务的地址和端口进行保存
        List<InterfaceInfo> interfaceInfos = innerInterfaceInfoService.listInterfaceInfos();

        for (InterfaceInfo interfaceInfo : interfaceInfos) {
            Long id = interfaceInfo.getId();
            String url = interfaceInfo.getUrl();
            String host = interfaceInfo.getHost();
            RouteDefinition routeDefinition = new RouteDefinition();
            routeDefinition.setId(
                    String.format("api:gateway:api_route_%s", id)
            );
            routeDefinition.setUri(URI.create(host));
            routeDefinition.setPredicates(Collections.singletonList(new PredicateDefinition("Path=" + url)));
            routeDefinition.setFilters(filters);
            //刷新缓存
            customRedisRouteDefinitionRepository.save(Mono.just(routeDefinition)).subscribe();
            //todo 集群模式下，用mq广播刷新路由消息，这样每个节点都能刷新
        }
        return true;
    }

    @Override
    public void addRoute(String routeId, String targetUrl, String path) {
        // 构建新的路由定义
        RouteDefinition definition = new RouteDefinition();
        definition.setId(routeId);
        definition.setUri(URI.create(targetUrl));
        // 设置路径谓词
        definition.setPredicates(Collections.singletonList(new PredicateDefinition("Path=" + path)));
        // 添加过滤器，（这边的过滤器是固定的，）
        //TODO 过滤器改的灵活点
        definition.setFilters(filters);
        // 添加到路由
//        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        // 集群模式保存
        customRedisRouteDefinitionRepository.save(Mono.just(definition)).subscribe();
        // 集群模式下，用mq广播刷新路由消息，这样每个节点都能刷新
        gatewayRefreshProducer.sendMessage(GateWayRefreshEnum.SUCCESS.getValue());
        // 单机部署直接用这个即可，不发消息，直接刷新
//        publisher.publishEvent(new RefreshRoutesEvent(this));
    }

    @Override
    public void deleteRoute(String routeId) {
        // 删除指定路由
        routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();
        publisher.publishEvent(new RefreshRoutesEvent(this));
    }

    @Override
    public Mono<List<RouteDefinition>> getGateway() {
        return customRedisRouteDefinitionRepository.getRouteDefinitions().collectList();
    }
}
